CONTENTS |
A layout manager arranges the child components of a container, as shown in Figure 18-1. It positions and sets the size of components within the container's display area according to a particular layout scheme. The layout manager's job is to fit the components into the available area while maintaining some spatial relationships among them. AWT and Swing come with several standard layout managers that will collectively handle most situations; you can make your own layout managers if you have special requirements.
Every container has a default layout manager; therefore, when you make a new container, it comes with a LayoutManager object of the appropriate type. You can install a new layout manager at any time with the setLayout() method. For example, we can set the layout manager of a Swing container's content pane to a BorderLayout like so:
mycontainer.getContentPane( ).setLayout (new BorderLayout( ));
Notice that we have called the BorderLayout constructor, but we haven't bothered to save a reference to the layout manager. This is typical; once you have installed a layout manager, it does its work behind the scenes, interacting with the container. You rarely call the layout manager's methods directly, so you don't usually need a reference (a notable exception is CardLayout). However, you do need to know what the layout manager is going to do with your components as you work with them.
The LayoutManager is consulted whenever a container's doLayout() method is called to reorganize the contents. It does its job by calling the setLocation() or setBounds() methods of the individual child components to arrange them in the container's display area. A container is laid out the first time it is displayed and thereafter whenever the container's revalidate() method is called. Containers that are a subclass of the Window class (Frame, JFrame, and JWindow) are automatically validated whenever they are packed or resized. Calling pack() sets the window's size as small as possible while granting all its components their preferred sizes.
Every component determines three important pieces of information used by the layout manager in placing and sizing it: a minimum size, a maximum size, and a preferred size. These sizes are reported by the getMinimumSize() , getMaximum-Size(), and getPreferredSize() methods of Component, respectively. For example, a plain JButton object can normally be changed to any size. However, the button's designer can provide a preferred size for a good-looking button. The layout manager might use this size when there are no other constraints, or it might ignore it, depending on its scheme. Now if we give the button a label, the button may need a new minimum size in order to display itself properly. The layout manager might show more respect for the button's minimum size and guarantee that it has at least that much space. Similarly, a particular component might not be able to display itself properly if it is too large (perhaps it has to scale up an image); it can use getMaximumSize() to report the largest size it considers acceptable.
The preferred size of a Container object has the same meaning as for any other type of component. However, since a Container may hold its own components and want to arrange them in its own layout, its preferred size is a function of its layout manager. The layout manager is therefore involved in both sides of the issue. It asks the components in its container for their preferred (or minimum) sizes in order to arrange them. Based on those values, it calculates the preferred size of its own container (which can be communicated to the container's parent and so on).
When a layout manager is called to arrange its components, it is working within a fixed area. It usually begins by looking at its container's dimensions and the preferred or minimum sizes of the child components. It then doles out screen area and sets the sizes of components according to its scheme and specific constraints. You can override the getMinimumSize(), getMaximumSize(), and getPreferredSize() methods of a component, but you should do this only if you are actually specializing the component, and it has new needs. If you find yourself fighting with a layout manager because it's changing the size of one of your components, you are probably using the wrong kind of layout manager or not composing your user interface properly. Often it's easier to use a number of JPanel objects in a given display, each one with its own LayoutManager. Try breaking down the problem: place related components in their own JPanel and then arrange the panels in the container. When that becomes unwieldy, you can choose to use a constraint-based layout manager such as GridBagLayout, which we'll discuss later in this chapter.
FlowLayout is a simple layout manager that tries to arrange components at their preferred sizes, from left to right and top to bottom in the container. A FlowLayout can have a specified row justification of LEFT, CENTER, or RIGHT and a fixed horizontal and vertical padding. By default, a flow layout uses CENTER justification, meaning that all components are centered within the area allotted to them. FlowLayout is the default for JPanels.
The following example adds five buttons to the content pane of a JFrame using the default FlowLayout:
//file: Flow.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Flow extends JPanel { public Flow( ) { // FlowLayout is default layout manager for a JPanel add(new JButton("One")); add(new JButton("Two")); add(new JButton("Three")); add(new JButton("Four")); add(new JButton("Five")); } public static void main(String[] args) { JFrame frame = new JFrame("Flow"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(400, 75); frame.setLocation(200, 200); Flow flow = new Flow( ); frame.setContentPane(flow); frame.setVisible(true); } }
The result is shown in Figure 18-2.
Try resizing the window. If it is made narrow enough, some of the buttons will spill over to a second or third row.
GridLayout arranges components into regularly spaced rows and columns. The components are arbitrarily resized to fit the grid; their minimum and preferred sizes are consequently ignored. GridLayout is most useful for arranging identically sized objects—perhaps a set of JPanels, each using a different layout manager.
GridLayout takes the number of rows and columns in its constructor. If you subsequently give it too many objects to manage, it adds extra columns to make the objects fit. You can also set the number of rows or columns to zero, which means that you don't care how many elements the layout manager packs in that dimension. For example, GridLayout(2,0) requests a layout with two rows and an unlimited number of columns; if you put ten components into this layout, you'll get two rows of five columns each.[1]
The following example sets a GridLayout with three rows and two columns as its layout manager:
//file: Grid.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Grid extends JPanel { public Grid( ) { setLayout(new GridLayout(3, 2)); add(new JButton("One")); add(new JButton("Two")); add(new JButton("Three")); add(new JButton("Four")); add(new JButton("Five")); } public static void main(String[] args) { JFrame frame = new JFrame("Grid"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(200, 200); frame.setLocation(200, 200); frame.setContentPane(new Grid( )); frame.setVisible(true); } }
The results are shown in Figure 18-3.
The five buttons are laid out, in order, from left to right, top to bottom, with one empty spot.
BorderLayout is a little more interesting. It tries to arrange objects in one of five geographical locations, represented by constants in the BorderLayout class: NORTH, SOUTH, EAST, WEST, and CENTER, possibly with some padding between. BorderLayout is the default layout for the content panes of JWindow and JFrame objects. Because each component is associated with a direction, BorderLayout can manage at most five components; it squashes or stretches those components to fit its constraints. As we'll see in the second example, this means that you often want to have BorderLayout manage sets of components in their own panels.
When we add a component to a border layout, we need to specify both the component and the position at which to add it. To do so, we use an overloaded version of the add() method that takes an additional argument as a constraint. This specifies the name of a position within the BorderLayout.
The following application sets a BorderLayout and adds our five buttons again, named for their locations:
//file: Border1.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Border1 extends JPanel { public Border1( ) { setLayout(new BorderLayout( )); add(new JButton("North"), BorderLayout.NORTH); add(new JButton("South"), BorderLayout.SOUTH); add(new JButton("East"), BorderLayout.EAST); add(new JButton("West"), BorderLayout.WEST); add(new JButton("Center"), BorderLayout.CENTER); } public static void main(String[] args) { JFrame frame = new JFrame("Border1"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(300, 300); frame.setLocation(200, 200); frame.setContentPane(new Border1( )); frame.setVisible(true); } }
The result is shown in Figure 18-4.
So, how exactly is the area divided up? Well, the objects at NORTH and SOUTH get their preferred height and fill the display area horizontally. EAST and WEST components, on the other hand, get their preferred width, and fill the remaining area between NORTH and SOUTH vertically. Finally, the CENTER object takes all the rest of the space. As you can see in Figure 18-4, our buttons get distorted into interesting shapes.
What if we don't want BorderLayout messing with the sizes of our components? One option would be to put each button in its own JPanel. The default layout for a JPanel is FlowLayout, which respects the preferred size of components. The preferred sizes of the panels are effectively the preferred sizes of the buttons, but if the panels are stretched, they won't pull their buttons with them. The following application illustrates this approach:
//file: Border2.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Border2 extends JPanel { public Border2( ) { setLayout(new BorderLayout( )); JPanel p = new JPanel( ); p.add(new JButton("North")); add(p, BorderLayout.NORTH); p = new JPanel( ); p.add(new JButton("South")); add(p, BorderLayout.SOUTH); p = new JPanel( ); p.add(new JButton("East")); add(p, BorderLayout.EAST); p = new JPanel( ); p.add(new JButton("West")); add(p, BorderLayout.WEST); p = new JPanel( ); p.add(new JButton("Center")); add(p, BorderLayout.CENTER); } public static void main(String[] args) { JFrame frame = new JFrame("Border2"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(225, 150); frame.setLocation(200, 200); frame.setContentPane(new Border2( )); frame.setVisible(true); } }
The result is shown in Figure 18-5.
In the example, we create a number of panels, put our buttons inside the panels, and put the panels into the frame window, which has the BorderLayout manager. Now, the JPanel for the CENTER button soaks up the extra space that comes from the BorderLayout. Each JPanel's FlowLayout centers the button in the panel and uses the button's preferred size. In this case, it's all a bit awkward. We'll see how we could accomplish this more directly using GridBagLayout shortly.
Most layout managers were defined back when Java 1.0 was first released. Swing adds a couple of new general-purpose layout managers in the javax.swing package; one is BoxLayout. This layout manager is useful for creating toolbars or vertical button bars. It lays out components in a single row or column. It is similar to FlowLayout but does not wrap components into new rows.
Although you can use BoxLayout directly, Swing includes a handy container called Box that takes care of the details for you. Every Box uses BoxLayout, but you don't really have to worry about it; the Box class includes some very useful methods for laying out components.
You can create a horizontal or vertical box using Box's static methods.
Container horizontalBox = Box.createHorizontalBox( ); Container verticalBox = Box.createVerticalBox( );
Once the Box is created, you can just add() components as usual:
Container box = Box.createHorizontalBox( ); box.add(new JButton("In the"));
Box includes several other static methods that create special invisible components that are handy for BoxLayout. The first of these is glue; glue is really space between components in the Box. When the Box is resized, glue expands or contracts as more or less space is available. The other special invisible component type is a strut. Like glue, a strut represents space between components, but it doesn't resize.
The following example creates a horizontal Box (shown in Figure 18-6) that includes both glue and struts. Play around by resizing the window to see the effect of the glue and the struts.
//file: Boxer.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Boxer extends JPanel { public static void main(String[] args) { JFrame frame = new JFrame("Boxer"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(250, 250); frame.setLocation(200, 200); Container box = Box.createHorizontalBox( ); box.add(Box.createHorizontalGlue( )); box.add(new JButton("In the")); box.add(Box.createHorizontalGlue( )); box.add(new JButton("clearing")); box.add(Box.createHorizontalStrut(10)); box.add(new JButton("stands")); box.add(Box.createHorizontalStrut(10)); box.add(new JButton("a")); box.add(Box.createHorizontalGlue( )); box.add(new JButton("boxer")); box.add(Box.createHorizontalGlue( )); frame.getContentPane( ).add(box, BorderLayout.CENTER); frame.pack( ); frame.setVisible(true); } }
Components are added sequentially for display from left to right or top to bottom with optional "glue" or "strut" constraints placed between them. By default, components simply line up one after another with no space between them. "Glue" acts like a spring, allowing its adjacent components to move to occupy the space evenly. A "strut" imposes a fixed space between adjacent components.
CardLayout is a special layout manager for creating the effect of a stack of cards. Instead of arranging all of the container's components, it displays only one at a time. You might use this kind of layout to implement something like a hypercard stack or a Windows-style set of configuration screens. If CardLayout sounds interesting, you might also want to investigate the JTabbedPane component, described in Chapter 16.
To add a component to a CardLayout, use a two-argument version of the container's add() method; the extra argument is an arbitrary string that serves as the card's name:
add("netconfigscreen", myComponent);
To bring a particular card to the top of the stack, call the CardLayout's show() method with two arguments: the parent Container and the name of the card you want to show. There are also methods—first() , last(), next(), and previous() for working with the stack of cards. These are all CardLayout instance methods. To invoke them, you need a reference to the CardLayout object itself, not to the container it manages. Each method takes a single argument: the parent Container. Here's an example:
//file: Card.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Card extends JPanel { CardLayout cards = new CardLayout( ); public Card( ) { setLayout(cards); ActionListener listener = new ActionListener( ) { public void actionPerformed(ActionEvent e) { cards.next(Card.this); } }; JButton button; button = new JButton("one"); button.addActionListener(listener); add(button, "one"); button = new JButton("two"); button.addActionListener(listener); add(button, "two"); button = new JButton("three"); button.addActionListener(listener); add(button, "three"); } public static void main(String[] args) { JFrame frame = new JFrame("Card"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(200, 200); frame.setLocation(200, 200); frame.setContentPane(new Card( )); frame.setVisible(true); } }
We add three buttons to the layout and cycle through them as they are pressed. An anonymous inner class serves as an action listener for each button; it simply calls CardLayout's next() method whenever a button is pressed. The reference to Card.this refers to the Card object, which is the container in this case. In a more realistic example, we would build a group of panels, each of which might implement some part of a complex user interface and add those panels to the layout. Each panel would have its own layout manager. The panels would be resized to fill the entire area available (i.e., the area of the Container they are in), and their individual layout managers would arrange their internal components.
GridBagLayout is a very flexible layout manager that allows you to position components relative to one another using constraints. With GridBagLayout (and a fair amount of effort), you can create almost any imaginable layout. Components are arranged at logical coordinates on an abstract grid. We'll call them "logical" coordinates because they really designate positions in the space of rows and columns formed by the set of components. Rows and columns of the grid stretch to different sizes, based on the sizes and constraints of the components they hold.
A row or column in a GridBagLayout expands to accommodate the dimensions and constraints of the largest component it contains. Individual components may span more than one row or column. Components that aren't as large as their grid cell can be anchored (positioned to one side) within their cell. They can also be set to fill or to expand their size in either dimension. Extra area in the grid rows and columns can be parceled out according to the weight constraints of the components. In this way, you can control how various components will grow and stretch when a window is resized.
GridBagLayout is much easier to use in a graphical WYSIWYG GUI builder environment. That's because working with GridBag is kind of like messing with the "rabbit ears" antennae on your television. It's not particularly difficult to get the results that you want through trial and error, but writing out hard and fast rules for how to go about it is difficult. In short, GridBagLayout is complex and has some quirks. It is also simply a bit ugly both in model and implementation. Remember that you can do a lot with nested panels and by composing simpler layout managers within one another. If you look back through this chapter, you'll see some examples of composite layouts; it's up to you to determine how far you should go before making the break from simpler layout managers to a more complex all-in-one layout manager like GridBagLayout.
Having said that GridBagLayout is complex and a bit ugly, we're going to contradict ourselves a little and say that its API is surprisingly simple. There is only one constructor with no arguments (GridBagLayout()), and there aren't a lot of fancy methods to control how the display works.
The appearance of a grid bag layout is controlled by sets of GridBagConstraints, and that's where things get hairy. Each component managed by a GridBagLayout is associated with a GridBagConstraints object. GridBagConstraints holds the following variables, which we'll describe in detail shortly:
Controls the position of the component on the layout's grid.
Controls how additional space in the row or column is allotted to the component.
Controls whether the component expands to fill the allotted space.
Controls the number of rows or columns the component spans.
Controls the position of the component if there is extra room within the allotted space.
Controls padding between the component and the borders of its area.
Controls padding between the component and neighboring components.
To make a set of constraints for a component or components, create a new instance of GridBagConstraints and set these public variables to the appropriate values. (There is also a large constructor that takes all 11 arguments.)
The easiest way to associate a set of constraints with a component is to use the version of add() that takes both a component object and a layout object as arguments. This puts the component in the container and associates the GridBagConstraints object with it:
Container content = getContentPane( ); JComponent component = new JLabel("constrain me, please..."); GridBagConstraints constraints = new GridBagConstraints( ); constraints.gridx = x; constraints.gridy = y; ... content.add(component, constraints);
You can also add a component to a GridBagLayout using the single argument add( ) method and then calling the layout's setConstraints() method directly to pass it the GridBagConstraints object for that component:
add(component); ... myGridBagLayout.setConstraints(component, constraints);
In either case, the set of constraints is copied when it is applied to the component. It's the individual constraints that apply to the component, not the GridBagConstraints object. Therefore, you're free to create a single set of GridBagConstraints, modify it as needed, and apply it as needed to different objects. You might want to create a helper method that sets the constraints appropriately, then adds the component, with its constraints, to the layout. That's the approach we'll take in our examples; our helper method is called addGB(), and it takes a component plus a pair of coordinates as arguments. These coordinates become the gridx and gridy values for the constraints. We could expand upon this later and overload addGB() to take more parameters for other constraints that we often change from component to component.
One of the biggest surprises in the GridBagLayout is that there's no way to specify the size of the grid. There doesn't have to be. The grid size is determined implicitly by the constraints of all the objects; the layout manager picks dimensions large enough so that everything fits. Thus, if you put one component in a layout and set its gridx and gridy constraints to 25, the layout manager creates a 25 x 25 grid, with rows and columns both numbered from 0 to 24. If you then add a second component with a gridx of 30 and a gridy of 13, the grid's dimensions change to 30 x 25. You don't have to worry about setting up an appropriate number of rows and columns. The layout manager does it automatically as you add components.
With this knowledge, we're ready to create some simple displays. We'll start by arranging a group of components in a cross shape. We maintain explicit x and y local variables, setting them as we add the components to our grid. This is partly for clarity, but it can be a handy technique when you want to add a number of components in a row or column. You can simply increment gridx or gridy before adding each component. This is a simple and problem-free way to achieve relative placement. (Later, we'll describe GridBagConstraints's RELATIVE constant, which does relative placement automatically.) The following code shows the first layout (see Figure 18-7).
//file: GridBag1.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GridBag1 extends JPanel { GridBagConstraints constraints = new GridBagConstraints( ); public GridBag1( ) { setLayout(new GridBagLayout( )); int x, y; // for clarity addGB(new JButton("North"), x = 1, y = 0); addGB(new JButton("West"), x = 0, y = 1); addGB(new JButton("Center"), x = 1, y = 1); addGB(new JButton("East"), x = 2, y = 1); addGB(new JButton("South"), x = 1, y = 2); } void addGB(Component component, int x, int y) { constraints.gridx = x; constraints.gridy = y; add(component, constraints); } public static void main(String[] args) { JFrame frame = new JFrame("GridBag1"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(225, 150); frame.setLocation(200, 200); frame.setContentPane(new GridBag1( )); frame.setVisible(true); } }
The buttons in this example are "clumped" together in the center of their display area. Each button is displayed at its preferred size, without stretching to fill the available space. This is how the layout manager behaves when the "weight" constraints are left unset. We'll talk more about weights in the next two sections.
Let's make the buttons expand to fill the entire JFrame window. To do so, we must take two steps: we must set the fill constraint for each button to the value BOTH, and we must set the weightx and weighty to nonzero values, as shown in this example:
//file: GridBag2.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GridBag2 extends JPanel { GridBagConstraints constraints = new GridBagConstraints( ); public GridBag2( ) { setLayout(new GridBagLayout( )); constraints.weightx = 1.0; constraints.weighty = 1.0; constraints.fill = GridBagConstraints.BOTH; int x, y; // for clarity addGB(new JButton("North"), x = 1, y = 0); addGB(new JButton("West"), x = 0, y = 1); addGB(new JButton("Center"), x = 1, y = 1); addGB(new JButton("East"), x = 2, y = 1); addGB(new JButton("South"), x = 1, y = 2); } void addGB(Component component, int x, int y) { constraints.gridx = x; constraints.gridy = y; add(component, constraints); } public static void main(String[] args) { JFrame frame = new JFrame("GridBag2"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(225, 150); frame.setLocation(200, 200); frame.setContentPane(new GridBag2( )); frame.setVisible(true); } }
Figure 18-8 shows the resulting layout.
BOTH is one of the constants of the GridBagConstraints class; it tells the component to fill the available space in both directions. Here are the constants you can use to set the fill field:
Fill the available horizontal space.
Fill the available vertical space.
Fill the available space in both directions.
Don't fill the available space; display the component at its preferred size.
We set the weight constraints to 1.0; in this example it doesn't matter what they are, provided each component has the same nonzero weight. Filling doesn't occur if the component weights in the direction you're filling are 0, which is the default value.
One of the most important features of GridBaglayout is that it lets you create arrangements in which components span two or more rows or columns. To do so, set the gridwidth and gridheight variables of the GridBagConstraints . The following example creates such a display; button one spans two columns vertically, and button four spans two horizontally. Figure 18-9 shows the resulting layout.
//file: GridBag3.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GridBag3 extends JPanel { GridBagConstraints constraints = new GridBagConstraints( ); public GridBag3( ) { setLayout(new GridBagLayout( )); constraints.weightx = 1.0; constraints.weighty = 1.0; constraints.fill = GridBagConstraints.BOTH; int x, y; // for clarity constraints.gridheight = 2; // span two rows addGB(new JButton("one"), x = 0, y = 0); constraints.gridheight = 1; // set it back addGB(new JButton("two"), x = 1, y = 0); addGB(new JButton("three"), x = 2, y = 0); constraints.gridwidth = 2; // span two columns addGB(new JButton("four"), x = 1, y = 1); constraints.gridwidth = 1; // set it back } void addGB(Component component, int x, int y) { constraints.gridx = x; constraints.gridy = y; add(component, constraints); } public static void main(String[] args) { JFrame frame = new JFrame("GridBag3"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(200, 200); frame.setLocation(200, 200); frame.setContentPane(new GridBag3( )); frame.setVisible(true); } }
The size of each element is controlled by the gridwidth and gridheight values of its constraints. For button one, we set gridheight to 2; therefore, it is two cells high. Its gridx and gridy positions are both zero, so it occupies cell (0,0) and the cell directly below it, (0,1). Likewise, button four has a gridwidth of 2 and a gridheight of 1, so it occupies two cells horizontally. We place this button in cell (1,1), so it occupies that cell and its neighbor, (2,1).
In this example, we set the fill to BOTH and weightx and weighty to 1 for all components. By doing so, we tell each button to occupy all the space available. Strictly speaking, this isn't necessary. However, it makes it easier to see exactly how much space each button occupies.
The weightx and weighty variables of a GridBagConstraints object determine how "extra" space in the container is distributed among the columns or rows in the layout. As long as you keep things simple, the effect these variables have is fairly intuitive: the larger the weight, the greater the amount of space allocated to the component. Figure 18-10 shows what happens if we vary the weightx constraint from 0.1 to 1.0 as we place three buttons in a row.
Here's the code:
//file: GridBag4.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GridBag4 extends JPanel { GridBagConstraints constraints = new GridBagConstraints( ); public GridBag4( ) { setLayout(new GridBagLayout( )); constraints.fill = GridBagConstraints.BOTH; constraints.weighty = 1.0; int x, y; // for clarity constraints.weightx = 0.1; addGB(new JButton("one"), x = 0, y = 0); constraints.weightx = 0.5; addGB(new JButton("two"), ++x, y); constraints.weightx = 1.0; addGB(new JButton("three"), ++x, y); } void addGB(Component component, int x, int y) { constraints.gridx = x; constraints.gridy = y; add(component, constraints); } public static void main(String[] args) { JFrame frame = new JFrame("GridBag4"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(300, 100); frame.setLocation(200, 200); frame.setContentPane(new GridBag4( )); frame.setVisible(true); } }
The specific values of the weights are not meaningful; it is only their relative proportions that matter. After the preferred sizes of the components (including padding and insets—see the next section) are determined, any extra space is doled out in proportion to the component's weights. So, for example, if each of our three components had the same weight, each would receive a third of the extra space. To make this more obvious, you may prefer to express the weights for a row or column as fractions totaling 1.0—for example: 0.25, 0.25, 0.50. Components with a weight of receive no extra space.
The situation is a bit more complicated when there are multiple rows or columns and when there is even the possibility of components spanning more than one cell. In the general case, GridBagLayout calculates an effective overall weight for each row and column and then distributes the extra space to them proportionally. Note that the previous single-row example is just a special case where the columns each have one component. The gory details of the calculations follow.
For a given row or column ("rank"), GridBagLayout first considers the weights of all the components contained strictly within that rank—ignoring those that span more than one cell. The greatest individual weight becomes the overall weight of the row or column. Intuitively this means that GridBagLayout is trying to accommodate the needs of the weightiest component in that rank.
Next, GridBagLayout considers the components that occupy more than one cell. Here things get a little weird. GridbagLayout wants to evaluate them to see whether they affect the determination of the largest weight in a row or column. However, because these components occupy more than one cell, GridBagLayout divides their weight among the ranks (rows or columns) that they span.
GridBagLayout tries to calculate an effective weight for the portion of the component that occupies each row or column. It does this by trying to divide the weight of the component among the ranks in the same proportions that the length (or height) of the component will be shared by the ranks. But how does it know what the proportions will be before the whole grid is determined? That's what it's trying to calculate after all. It simply guesses based on the row or column weights already determined. GridBagLayout uses the weights determined by the first round of calculations to split up the weight of the component over the ranks that it occupies. For each row or column, it then considers that fraction of the weight to be the component's weight for that rank. That weight then contends for the "heaviest weight" in the row or column, possibly changing the overall weight of that row or column, as we described earlier.
If a component is smaller than the space available for it, it is centered by default. But centering isn't the only possibility. The anchor constraint tells a grid bag layout how to position a component within its cell in the grid. Possible values are GridBagConstraints.CENTER, NORTH, NORTHEAST, EAST, SOUTHEAST, SOUTH, SOUTHWEST, WEST, and NORTHWEST. For example, an anchor of GridBagConstraints.NORTH centers a component at the top of its display area; SOUTHEAST places a component at the bottom-right corner of its area.
Another way to control the behavior of a component in a grid bag layout is to use padding and insets. Padding is determined by the ipadx and ipady fields of GridBagConstraints. They specify horizontal and vertical "growth factors" for the component. In Figure 18-11, the West button is larger because we have set the ipadx and ipady values of its constraints to 25. Therefore, the layout manager gets the button's preferred size and adds 25 pixels in each direction to determine the button's actual size. The sizes of the other buttons are unchanged because their padding is set to 0 (the default), but their spacing is different. The West button is unnaturally tall, which means that the middle row of the layout must be taller than the others.
//file: GridBag5.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GridBag5 extends JPanel { GridBagConstraints constraints = new GridBagConstraints( ); public GridBag5( ) { setLayout(new GridBagLayout( )); int x, y; // for clarity addGB(new JButton("North"), x = 1, y = 0); constraints.ipadx = 25; // add padding constraints.ipady = 25; addGB(new JButton("West"), x = 0, y = 1); constraints.ipadx = 0; // remove padding constraints.ipady = 0; addGB(new JButton("Center"), x = 1, y = 1); addGB(new JButton("East"), x = 2, y = 1); addGB(new JButton("South"), x = 1, y = 2); } void addGB(Component component, int x, int y) { constraints.gridx = x; constraints.gridy = y; add(component, constraints); } public static void main(String[] args) { JFrame frame = new JFrame("GridBag5"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(250, 250); frame.setLocation(200, 200); frame.setContentPane(new GridBag5( )); frame.setVisible(true); } }
Notice that the horizontal padding, ipadx, is added on both the left and right sides of the button. Therefore, the button grows horizontally by twice the value of ipadx. Likewise, the vertical padding, ipady, is added on both the top and the bottom.
Insets add space between the edges of the component and its cell. They are stored in the insets field of GridBagConstraints, which is an Insets object. An Insets object has four fields to specify the margins on the top, bottom, left, and right of the component. The relationship between insets and padding can be confusing. As shown in Figure 18-12, padding is added to the component itself, increasing its size. Insets are external to the component and represent the margin between the component and its cell.
Padding and weighting have an odd interaction with each other. If you use padding, it's best to use the default weightx and weighty values for each component.
In all our grid bag layouts so far, we have specified the gridx and gridy coordinates of each component explicitly using its constraints. Another alternative is relative positioning.
Conceptually, relative positioning is simple: we simply say "put this component to the right of (or below) the previous component." To do so you can set gridx or gridy to the constant GridBagConstraints.RELATIVE. Unfortunately, it's not as simple as this. Here are a couple of warnings:
To place a component to the right of the previous one, set gridx to RELATIVE and use the same value for gridy that you used for the previous component.
Similarly, to place a component below the previous one, set gridy to RELATIVE and leave gridx unchanged.
Setting both gridx and gridy to RELATIVE places all the components in one row, not in a diagonal line, as you might expect. (This is the default.)
In other words, if gridx or gridy is RELATIVE, you had better leave the other value unchanged. RELATIVE makes it easy to arrange a lot of components in a row or a column. That's what it was intended for; if you try to do something else, you're fighting against the layout manager, not working with it.
GridBagLayout allows another kind of relative positioning in which you specify where, in a row or a column, the component should be placed overall. To do so, you use the gridwidth and gridheight fields of GridBagConstraints. Setting either of these to the constant REMAINDER says that the component should be the last item in its row or column and therefore should occupy all the remaining space. Setting either gridwidth or gridheight to RELATIVE says that it should be the second to the last item in its row or column. Unfortunately, you can use these constants to create constraints that can't possibly be met; for example, you can say that two components must be the last component in a row. In these cases, the layout manager tries to do something reasonable, but it will almost certainly do something you don't want.
Sometimes things don't fall neatly into little boxes. This is true of layouts as well as life. For example, if you want to use some of GridBagLayout's weighting features for part of your GUI, you could create separate layouts for different parts of the GUI and combine them with yet another layout. That's how we'll build the pocket calculator interface in Figure 18-13. We will use three grid bag layouts: one for the first row of buttons (C, %, +), one for the last (0, ., =) and one for the window itself. The master layout (the window's) manages the text field we use for the display, the panels containing the first and last rows of buttons, and the twelve buttons in the middle.[2]
Here's the code for the Calculator example. It implements only the user interface (i.e., the keyboard); it collects everything you type in the display field until you press C (clear). Figuring out how to connect the GUI to some other code that would perform the operations is up to you. One strategy would be to send an event to the object that does the computation whenever the user presses the equals sign. That object could read the contents of the text field, parse it, do the computation, and display the results.
//file: Calculator.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Calculator extends JPanel implements ActionListener { GridBagConstraints gbc = new GridBagConstraints( ); JTextField theDisplay = new JTextField( ); public Calculator( ) { gbc.weightx = 1.0; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.BOTH; ContainerListener listener = new ContainerAdapter( ) { public void componentAdded(ContainerEvent e) { Component comp = e.getChild( ); if (comp instanceof JButton) ((JButton)comp).addActionListener(Calculator.this); } }; addContainerListener(listener); gbc.gridwidth = 4; addGB(this, theDisplay, 0, 0); // make the top row JPanel topRow = new JPanel( ); topRow.addContainerListener(listener); gbc.gridwidth = 1; gbc.weightx = 1.0; addGB(topRow, new JButton("C"), 0, 0); gbc.weightx = 0.33; addGB(topRow, new JButton("%"), 1, 0); gbc.weightx = 1.0; addGB(topRow, new JButton("+"), 2, 0 ); gbc.gridwidth = 4; addGB(this, topRow, 0, 1); gbc.weightx = 1.0; gbc.gridwidth = 1; // make the digits for(int j=0; j<3; j++) for(int i=0; i<3; i++) addGB(this, new JButton("" + ((2-j)*3+i+1) ), i, j+2); // -, x, and divide addGB(this, new JButton("-"), 3, 2); addGB(this, new JButton("x"), 3, 3); addGB(this, new JButton("\u00F7"), 3, 4); // make the bottom row JPanel bottomRow = new JPanel( ); bottomRow.addContainerListener(listener); gbc.weightx = 1.0; addGB(bottomRow, new JButton("0"), 0, 0); gbc.weightx = 0.33; addGB(bottomRow, new JButton("."), 1, 0); gbc.weightx = 1.0; addGB(bottomRow, new JButton("="), 2, 0); gbc.gridwidth = 4; addGB(this, bottomRow, 0, 5); } void addGB(Container cont, Component comp, int x, int y) { if ((cont.getLayout( ) instanceof GridBagLayout) == false) cont.setLayout(new GridBagLayout( )); gbc.gridx = x; gbc.gridy = y; cont.add(comp, gbc); } public void actionPerformed(ActionEvent e) { if (e.getActionCommand( ).equals("C")) theDisplay.setText(""); else theDisplay.setText(theDisplay.getText( ) + e.getActionCommand( )); } public static void main(String[] args) { JFrame frame = new JFrame("Calculator"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(200, 250); frame.setLocation(200, 200); frame.setContentPane(new Calculator( )); frame.setVisible(true); } }
Once again, we use an addGB() helper method to add components with their constraints to the layout. Before discussing how to build the layout, let's take a look at addGB( ). We said earlier that three layout managers are in our user interface: one for the application panel itself, one for the panel containing the first row of buttons (topRow), and one for the panel containing the bottom row of buttons (bottomRow). We use addGB() for all three layouts; its first argument specifies the container to add the component to. Thus, when the first argument is this, we're adding an object to the content pane of the JFrame. When the first argument is topRow, we're adding a button to the first row of buttons. addGB() first checks the container's layout manager and sets it to GridBagLayout if it isn't already set properly. It sets the object's position by modifying a set of constraints, gbc, and then uses these constraints to add the object to the container.
We use a single set of constraints throughout the example, modifying fields as we see fit. The constraints are initialized in Calculator's constructor. Before calling addGB(), we set any fields of gbc for which the defaults are inappropriate. Thus, for the answer display, we set the grid width to 4 and add the answer display directly to the application panel (this). The add() method, which is called by addGB(), makes a copy of the constraints, so we're free to reuse gbc throughout the application.
The first and last rows of buttons motivate the use of multiple GridBagLayout containers, each with its own grid. These buttons appear to straddle grid lines, but you really can't accomplish this using a single grid. Therefore, topRow has its own layout manager, with three horizontal cells, allowing each button in the row to have a grid width of 1. To control the size of the buttons, we set the weightx variables so that the clear and plus buttons take up more space than the percent button. We then add the topRow as a whole to the application, with a grid width of 4. The bottom row is built similarly.
To build the buttons for the digits 1 through 9, we use a doubly nested loop. There's nothing particularly interesting about this loop, except that it's probably a bit too clever for good taste. The minus, multiply, and divide buttons are also simple: we create a button with the appropriate label and use addGB() to place it in the application. It's worth noting that we used a Unicode constant to request a real division sign rather than wimping out and using a slash.
That's it for the user interface; what's left is event handling. Each button generates action events; we need to register listeners for these events. We'll make the application panel, the Calculator, the listener for all the buttons. To register the Calculator as a listener, we'll be clever. Whenever a component is added to a container, the container generates a ContainerEvent. We use an anonymous inner class ContainerListener to register listeners for our buttons. This means that the Calculator must register as a ContainerListener for itself and for the two panels, topRow and bottomRow. The componentAdded() method is very simple. It calls getChild() to find out what component caused the event (i.e., what component was added). If that component is a button, it registers the Calculator as an ActionListener for that button.
actionPerformed() is called whenever the user presses any button. It clears the display if the user pressed the C button; otherwise, it appends the button's action command (in this case, its label) to the display.
Combining layout managers is an extremely useful trick. Granted, this example verges on overkill. You won't often need to create a composite layout using multiple grid bags. Composite layouts are most common with BorderLayout; you'll frequently use different layout managers for each of a border layout's regions. For example, the CENTER region might be a ScrollPane, which has its own special-purpose layout manager; the EAST and SOUTH regions might be panels managed by grid layouts or flow layouts, as appropriate.
We've covered the basic layout managers; with them, you should be able to create just about any user interface you like.
But that's not all, folks. If you want to experiment with layout managers that are undocumented, may change, and may not be available locally on all platforms, look in the sun.awt classes. You'll find a HorizBagLayout, a VerticalBagLayout, an OrientableFlowLayout, and a VariableGridLayout. Furthermore, public domain layout managers of all descriptions are on the Net.
It's possible to set the layout manager to null: no layout control. You might do this to position an object on the display at some absolute coordinates. This is usually not the right approach. Components might have different minimum sizes on different platforms, and your interface would not be very portable.
The following example doesn't use a layout manager and works with absolute coordinates instead:
//file: MoveButton.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class MoveButton extends JPanel { JButton button = new JButton("I Move"); public MoveButton( ) { setLayout(null); add(button); button.setSize(button.getPreferredSize( )); button.setLocation(20, 20); addMouseListener(new MouseAdapter( ) { public void mousePressed(MouseEvent e) { button.setLocation(e.getX( ), e.getY( )); } }); } public static void main(String[] args) { JFrame frame = new JFrame("MoveButton"); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); frame.setSize(250, 200); frame.setLocation(200, 200); frame.setContentPane(new MoveButton( )); frame.setVisible(true); } }
Click in the window area, outside of the button, to move the button to a new location. Try resizing the window and note that the button stays at a fixed position relative to the window's upper left corner.
The SpringLayout is a new addition in Java 1.4. It supports a combination of absolute positioning and point-to-point attachments between components (kind of like the "glue" of the BoxLayout). SpringLayout is useful in GUI builder applications because it's easy to translate manual user placement into these kinds of constraints.
SpringLayout uses Spring objects to attach edges of components. Springs have a range of motion and can be anchored at a coordinate or at a component's edge. Any two component edges can also be connected by a Spring. The two edges may belong to two different components, in which case the spring constrains the way the components are placed relative to one another, or they may belong to the same component, in which case the spring constrains the width or height of the component.
We don't cover SpringLayout here, but you can read about it in O'Reilly's Java Swing.
[1] Calling new GridLayout(0, 0) causes a runtime exception; either the rows or columns parameter must be greater than zero.
[2] If you're curious, this calculator is based on the ELORG-801, encountered in an online "calculator museum"; see http://www.taswegian.com/MOSCOW/elorg801.html.
CONTENTS |